package com.google.code.hibernate.detachedquery;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;

import org.hibernate.Query;
import org.hibernate.Session;

/**
 * 
 * @author wangzijian
 *
 */
public class DetachedQueries {

	private static final Class<?>[] QUERY_INTERFACES = new Class<?>[] { DetachedQuery.class };

	private DetachedQueries() {
	}

	public static DetachedQuery from(String queryString) {
		return (DetachedQuery) Proxy.newProxyInstance(
				currentClassLoader(), 
				QUERY_INTERFACES, 
				new Handler(queryString));
	}

	private static ClassLoader currentClassLoader() {
		return Thread.currentThread().getContextClassLoader();
	}

	private static class Handler implements InvocationHandler {

		private final List<Invocation> invocations = new ArrayList<Invocation>();
		private final String queryString;

		public Handler(String queryString) {
			this.queryString = queryString;
		}

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			if (method.getName().equals("equals")) {
				return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
			} else if (method.getName().equals("hashCode")) {
				return Integer.valueOf(System.identityHashCode(proxy));
			} else if (method.getName().equals("getExecutableQuery")) {
				Session session = (Session) args[0];
				Query query = session.createQuery(queryString);
				for (Invocation methodInvocation : invocations) {
					methodInvocation.invoke(query);
				}
				return query;
			} else {
				invocations.add(new Invocation(method, args));
				return proxy;
			}
		}
	}

	private static class Invocation {

		private final Method method;
		private final Object[] args;

		public Invocation(Method method, Object[] args) {
			this.method = method;
			this.args = args;
		}

		public void invoke(Query query) throws Throwable {
			Method methodOfQuery = Query.class.getMethod(method.getName(), method.getParameterTypes());
			methodOfQuery.invoke(query, args);
		}
	}
}
